developmentor - a developer services company				  
				      Essential .NET Security
   Modules

Web Apps

Estimated time for completion: 60 minutes


Goals of this exercise:

  • Experiment with the ASP.NET HTTP Pipeline
  • Write HttpModules to implement security features
  • Modify the default behavior of the built-in security modules

Overview

In this lab, you will build the infrastructure that will allow a single web application to support both forms authentication and integrated Windows authentication. To build this solution, you'll need to do some thinking about the ASP.NET HTTP pipeline.


part 1- Enabling Forms Authentication

The first step is to enable forms authentication, since you know you'll need to support that for external clients.

steps:

  1. First of all, create a virtual directory called "webapp" and point it to the directory where you'll be working: before\web. Examine the contents of this directory so you are familiar with what's already there. Note that a simple login page (login.aspx) has already been provided for you, and that it authenticates clients based on information in web.config (which is fine for this lab, but in real life, it's preferable to store your user accounts in a database like SQL Server).

  2. Open up web.config in the editor of your choice, and enable Forms authentication with all the default settings, and provide a user database as well. For example:

        <authentication mode='Forms'>
          <forms>
            <credentials passwordFormat='SHA1'>
              <user name='Alice' password='...'/>
              <user name='Bob'   password='...'/>
            </credentials>
          </forms>
        </authentication>
    
  3. In the hashpass directory of this lab you should find a file called hashpass.cs. Compile (csc hashpass.cs) and run it to calculate password hashes for your users. For example, for the password s&7uT#xM>x

    c:\temp>hashpass "s&7uT#xM>x"
    1D0AE8B294068D02B0612F1C3DADAE43EEAD757D
    

    Paste the 40 character output into the password attribute for your user, and repeat the process for each user account you placed in web.config. Once you're done making changes, don't forget to save your web.config file to disk.

  4. Now surf to the default.aspx page (provided for you) which simply prints out some information about the security contexts on the server. Were you asked to authenticate? Why not?

  5. Unless the request bounces with a 401 (Access Denied), forms authentication will simply represent anonymous requests with an anonymous identity. So add an <authorization> section to web.config that denies anonymous requests:

        <authorization>
          <deny users='?'/>
        </authorization>
  6. Now try again, and you should be sent to the forms login page (provided for you). Make sure you can log in using the various user accounts you designated in web.config. You should also be able to log out using the logout button that appears when the page detects that you were authenticated with Forms auth (if you're curious how this works, check out the Form_Load handler in default.aspx).


part 2- Understanding the HTTP Pipeline

In this part, you will examine the modules wired into the pipeline and think about where you need to wire in your own HttpModules to change the default behavior of Forms authentication.

steps:

  1. If you look in your web directory, you'll see a file called showPipeline.ashx. Surf here via HTTP and examine the output, which shows all the HttpModule event handlers wired up to the various events in the HTTP pipeline. The events are listed in the order they are fired. Find all events that FormsAuthenticationModule listens on, and make a note of them.

  2. Think for a moment about what the AuthenticateRequest event means for Forms authentication - exactly what does FormsAuthModule need to do at this step?

  3. Now think about the EndRequest event. Why does FormsAuthModule need to run before the response is sent back to the web server?

  4. FormsAuthModule's job in AuthenticateRequest is to look for a forms auth cookie sent from the client, and to turn it into a managed principal which hangs off HttpContext.User. Its job in EndRequest is to convert any request bounced with a 401 status code (Access Denied) into a redirect to the login page.

  5. Your job is to wire some code into the pipeline to allow requests from the local intranet to be processed using the web server's integrated Kerberos support. This will allow users inside your imaginary corporation to be able to leverage single sign on. All external requests will be shunted through forms authentication.

  6. FormsAuthenticationModule does provide a bit of support for doing this. At the beginning of its event handler for AuthenticateRequest, it fires its own Authenticate event that you can override to provide a principal of your own. If you provide a principal during this event, Forms will simply stop processing the AuthenticateRequest event, allowing you to specify the principal to be used for the request. However, you still have to deal with the EndRequest event, and FormsAuthenticationModule provides no help for you here, so you'll have to work around this obstacle.

  7. In order to allow Forms authentication to succeed, you need to enable anonymous access in the web server. But you also need to enable Integrated Windows Authentication to authenticate clients from your intranet (go do that now - via the IIS admin tool, enable both anonymous and integrated windows authentication for your webapp virtual directory).

    With both of these options enabled, IIS will treat all connections as anonymous unless the request is rejected with a 401 (Access Denied). In that case, IIS will attempt to authenticate the user and if successful, will retry the request. So in the case where you want to do Kerb authentication, you'll need to let the 401 percolate up to the web server, instead of having FormsAuthModule swallow it up in its EndRequest event handler. You'll basically need to hide the 401 status from the Forms event handler, then restore it so it flows up to IIS. This means you need to install modules that listen to EndRequest and are sandwiched directly before and after FormsAuthModule. These modules can hide and restore a 401 status code.


part 3- Overriding Forms Authentication

In this part, you'll write a module that will determine the location of the user (intranet or Internet), and authenticate via Forms only for the Internet case.

steps:

  1. Open the SecurityModules.sln solution and have a look at SecurityModules.cs. Note that the skeleton of an HttpModule has been provided for you.

  2. Implement an event handler for the FormsAuthenticationModule.Authenticate event. It should look like this:

    void OnFormsAuthenticate(object sender, FormsAuthenticationEventArgs args) {
    }
    

    In the Init function, wire yourself up to the FormsAuthenticationModule.Authenticate event. Since this is an event fired by another module in the pipeline, you'll need to locate that module in the HttpApplication.Modules collection. You can either find the module by name or by type, but finding it by name is a bit easier, as you can simply index into the Modules collection and wire yourself up as follows:

    ((FormsAuthenticationModule)app.Modules["FormsAuthentication"]).Authenticate +=
      new FormsAuthenticationEventHandler(OnFormsAuthenticate);
    
  3. In your event handler, get the HttpContext from the FormsAuthenticationEventArgs argument passed to your handler. Pass the HttpContext to the helper function (provided for you) _getWorkerRequest(). This sucks out the underlying HttpWorkerRequest, an object you'll need in order to implement Windows style authentication for your intranet clients. Note that while HttpWorkerRequest is documented, the mechanism for getting it from the HttpContext is not (this is the only somewhat dodgy aspect of this implementation, because we're relying on a private field of HttpContext).

  4. You now need to branch your code depending on whether the request should use Windows authentication or Forms authentication, so pass the context to _shouldUseWindowsAuth(), which you'll implement later. If the function returns true, you need to override Forms auth and provide a WindowsPrincipal based on information from the web server. Here's some code that does this:

    if (_shouldUseWindowsAuth(ctx)) {
        // use Windows authentication instead of Forms auth
        string userName = workerRequest.GetServerVariable("LOGON_USER");
        WindowsIdentity id = null;
        if (null == userName || 0 == userName.Length) {
            // special case anonymous clients
            id = WindowsIdentity.GetAnonymous();
        }
        else {
            id = new WindowsIdentity(workerRequest.GetUserToken(),
                                     workerRequest.GetServerVariable("AUTH_TYPE"),
                                     WindowsAccountType.Normal,
                                     true);
        }
        args.User = new WindowsPrincipal(id);
    }
    

    If you fill in the FormsAuthenticationEventArgs.User field with a principal, FormsAuthentication will use this principal and not bother looking for the forms auth cookie, which is exactly what you want.

  5. Implement the _shouldUseWindowsAuth method by looking at the ctx.Request.UserHostAddress field. This gives you a string representation of the dotted-decimal IP address that was specified as the source of the request. For the purposes of this lab, you might want to get some IP addresses of people sitting around you (your own IP address will show up as the local loopback "127.0.0.1"), and designate some of them as requiring Windows authentication and other as requiring Forms authentication, thus emulating handling requests from the local intranet and the Internet.

  6. Build your module (the project is configured so the output will go directly into the bin subdirectory of your web folder).

  7. Wire your module into the pipeline by adding the following to your web.config file:

        <httpModules>
          <add name='postFormsAuth' type='DevelopMentor.PostFormsAuthModule, SecurityModules'/>
        </httpModules>
    
  8. Temporarily remove the <deny> entry from your web.config file so that anonymous requests are allowed and then try surfing to default.aspx just to see that your code runs without any errors. You might want to attach a debugger to the worker process (w3wp.exe) and step through your code during a request just to make sure things are working so far.


part 4- Handling EndRequest

In this part, you'll complete your implementation by suppressing the forms login redirection for your Intranet clients.

steps:

  1. In SecurityModules.cs, add another class called PreFormsAuthModule as follows:

        public class PreFormsAuthModule : IHttpModule {
            void IHttpModule.Init(HttpApplication app) {
                app.EndRequest += new EventHandler(OnEndRequest);
            }
            void IHttpModule.Dispose() {}
    
            void OnEndRequest(object sender, EventArgs args) {
                HttpContext ctx = ((HttpApplication)sender).Context;
    
    			// TODO: your code goes here
            }
        }
    
  2. Note that code you just pasted into your project provides a boilerplate HttpModule that wires itself up for a single event: EndRequest. You are going to eventually wire this module up so it runs just before FormsAuthModule (thus the name of the class). Your job in OnEndRequest is to check to see if we did Windows authentication (as opposed to Forms auth) and if so, hide any 401 status code before it reaches FormsAuthModule's EndRequest handler.

  3. An easy way to see if your other module chose Windows authentication is to simply check the type of Identity object:

    if (typeof(WindowsIdentity) == ctx.User.Identity.GetType()) {
    }
  4. If you discover that Windows authentication is in force, then you'll also want to check for a 401 status code via HttpContext.Response.StatusCode. If you find a 401 status code, set it to 200 and record the fact that you hid the 401 in the HttpContext.Items dictionary, which is designed for this type of bookkeeping:

    ctx.Items["_hid401Status"] = "true";

    You'll use this hint in the PostFormsAuthModule to restore the 401 status after you've safely maneuvered around FormsAuthModule's EndRequest handler.

  5. Now go back to your other HttpModule, PostFormsAuthModule, and add a handler for EndRequest (just like the one you just wrote). Don't forget to wire up the event handler in your Init function just as was done in the PreFormsAuthModule.

  6. This new handler, which will run *after* FormsAuthModule, should simply restore any hidden 401 status code. This will allow it to percolate up to the web server, which will attempt to authenticate the client using integrated security (e.g., Kerberos). The following code shows how simple this is:

    // restore 401 status if we hid it from FormsAuthModule
    if (ctx.Items["_hid401Status"] != null) {
        ctx.Response.StatusCode = 401;
        ctx.Items["_hid401Status"] = null;
    }
    
  7. Recycle the worker process (right click the application pool which contains your virtual directory and choose "Recycle"), and then build your new module.

  8. Here's the tricky part. You now need to very carefully wire your HttpModules into the pipeline so they sit right before and after FormsAuthModule. To do this, bring up machine.config and copy the entire <httpModules> section. Paste that section into your own web.config file, overwriting what you used to have for that section. Add a <clear/> element before all of the <add> elements to indicate that you wish to completely rewire the pipeline from scratch. Then add entries for your two modules right before and right after FormsAuthenticationModule. You should end up with something that looks like this:

        <httpModules>
          <clear/>
          <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
          <add name="Session" type="System.Web.SessionState.SessionStateModule" />
          <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
          <add name='preFormsAuth' type='DevelopMentor.PreFormsAuthModule, SecurityModules'/>
          <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
          <add name='postFormsAuth' type='DevelopMentor.PostFormsAuthModule, SecurityModules'/>
          <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />
          <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
          <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />
        </httpModules>
    
  9. Uncomment the <deny> entry in your web.config files to force authentication, and you should now be able to test your app (here are some links: default.aspxshowPipeline.ashx. Verify that you can support both Windows and Forms authentication depending on the location of the client. Check to see how the wiring of the pipeline looks with your modules in place.

Solutions: